/*
 * Copyright (C) 2010 Charles Norona, Allan Pinero, Christopher Sizelove
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package edu.fau.rifl;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Scanner;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;

/**
 * This is the main Activity that displays the current chat session.
 */
public class BluetoothInterfaceActivity extends Activity implements SensorEventListener
{
	// Debugging
	private static final String TAG = "RIFL Mobile";
	private static final boolean D = true;

	// Message types sent from the BluetoothChatService Handler
	public static final int MESSAGE_STATE_CHANGE = 1;
	public static final int MESSAGE_READ = 2;
	public static final int MESSAGE_WRITE = 3;
	public static final int MESSAGE_DEVICE_NAME = 4;
	public static final int MESSAGE_TOAST = 5;

	// Key names received from the BluetoothChatService Handler
	public static final String DEVICE_NAME = "device_name";
	public static final String TOAST = "toast";

	// Intent request codes
	private static final int REQUEST_CONNECT_DEVICE = 1;
	private static final int REQUEST_ENABLE_BT = 2;

	//Sensor attributes
	private SensorManager mgr; 
	private List<Sensor> sensorList;
	private Sensor accelerometer;
	private Sensor orientation;

	// Layout Views
	private TextView output;
	private TextView input;

	// Name of the connected device
	private String mConnectedDeviceName = null;

	// Local Bluetooth adapter
	private BluetoothAdapter mBluetoothAdapter = null;

	// Member object for the chat services
	/**
	 * @uml.property  name="mStreamService"
	 * @uml.associationEnd  
	 */
	private BluetoothService mBluetoothService = null;

	//Bluetooth and Sensor variables
	private String bluetoothMessage = null;
	private double newTimeStamp = 0;
	private double oldTimeStamp = 0;
	private String sensorName = "";
	private String sensorType = "";
	private String sensorVendor = "";
	private String sensorVersion = "";
	private String maxRange = "";
	private String powerDissipation = "";
	private String resolution = "";
	private String accuracy = "";
	private String timeStamp = "";
	private String[] values = new String[3];
	private boolean isTransmitting = false;

	//Calibration variables
	private boolean isCalibrating = false;
	private boolean sensorReadingReady = false;
	private static int calibrationRange = 1000;
	private static int calibrationLoop = 0;
	private static int accelLoop = 0;
	private double calibMagnitude = 0;

	//Local sensor test variables
	private boolean isSensorTesting = false;

	//Base station variables
	//TODO: Remember to use these variables for the base station app.
	private String[] accValues = new String[3];
	private String[] oriValues = new String[3];
	private double timeChange = 0;
	private double magnitude = 0;
	private double distanceChange = 0;

	// File IO
	FileWriter fWriter;
	File filepath = new File("/sdcard/sensor_results");
	PrintWriter pWriter;
	String fileOutMsg = "";
	PowerManager powerManager;
	PowerManager.WakeLock wakeLock;

	@Override
	public void onCreate(Bundle savedInstanceState) 
	{
		super.onCreate(savedInstanceState);
		if(D) Log.e(TAG, "+++ ON CREATE +++");

		// Set up the window layout
		setContentView(R.layout.main);
		output = (TextView) findViewById(R.id.output);
		input = (TextView) findViewById(R.id.input);

		// Set up the sensor service
		mgr = (SensorManager) getSystemService(SENSOR_SERVICE);


		// Get local Bluetooth adapter
		mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

		// If the adapter is null, then Bluetooth is not supported
		if (mBluetoothAdapter == null) {
			Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
			finish();
			return;
		}

		//In case there are no sensors or bluetooth connections.
		input.setText("NULL");
		output.setText("NULL");
		
		powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
		wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
	}

	@Override
	public void onStart() {
		super.onStart();
		if(D) Log.e(TAG, "++ ON START ++");

		// If BT is not on, request that it be enabled.
		// setupChat() will then be called during onActivityResult
		if (!mBluetoothAdapter.isEnabled()) {
			Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
			startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
			// Otherwise, setup the chat session
		} else {
			if (mBluetoothService == null) setupBTService();
		}
		wakeLock.acquire();
	}

	/**
	 * Initializes the BluetoothService class in connection to the Handler.
	 */
	private void setupBTService() 
	{
		Log.d(TAG, "setupChat()");

		// Initialize the array adapter for the conversation thread
		//mConversationArrayAdapter = new ArrayAdapter<String>(this, R.layout.message);
		output.setText("Null");
		//input.setText("Null");

		// Initialize the BluetoothChatService to perform bluetooth connections
		mBluetoothService = new BluetoothService(this, mHandler);

		// Initialize the buffer for outgoing messages
		//mOutStringBuffer = new StringBuffer("");
		bluetoothMessage = "";
	}

	@Override
	public synchronized void onResume() {
		super.onResume();
		if(D) Log.e(TAG, "+ ON RESUME +");

		// Performing this check in onResume() covers the case in which BT was
		// not enabled during onStart(), so we were paused to enable it...
		// onResume() will be called when ACTION_REQUEST_ENABLE activity returns.
		if (mBluetoothService != null) {
			// Only if the state is STATE_NONE, do we know that we haven't started already
			if (mBluetoothService.getState() == BluetoothService.STATE_NONE) {
				// Start the Bluetooth chat services
				mBluetoothService.start();
			}
		}

		// Start updates for one or more sensors
		sensorList = mgr.getSensorList(Sensor.TYPE_ALL);//For both orientation and accelerometer.

		if (sensorList.size() > 0)
		{
			accelerometer = sensorList.get(0);
			orientation = sensorList.get(2);

			mgr.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);
			mgr.registerListener(this, orientation, SensorManager.SENSOR_DELAY_NORMAL);
		}		
	}

	@Override
	public synchronized void onPause() 
	{
		super.onPause();
		if(D) Log.e(TAG, "- ON PAUSE -");


		// Stop updates to save power while app paused
		mgr.unregisterListener(this);
	}

	@Override
	public void onStop() 
	{
		super.onStop();
		if(D) Log.e(TAG, "-- ON STOP --");

		//Flush the IO writer stream and close it
		try 
		{
			fWriter.flush();
			fWriter.close();

		} 
		catch (Exception e) 
		{
			e.printStackTrace();
		}
		wakeLock.release();
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		// Stop the Bluetooth chat services
		if (mBluetoothService != null) mBluetoothService.stop();
		if(D) Log.e(TAG, "--- ON DESTROY ---");
	}

	private void ensureDiscoverable() {
		if(D) Log.d(TAG, "ensure discoverable");
		if (mBluetoothAdapter.getScanMode() !=
			BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
			Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
			discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
			startActivity(discoverableIntent);
		}
	}

	/**
	 * Sends a message.
	 * @param message  A string of text to send.
	 */
	private void sendMessage(String message) 
	{
		// Check that we're actually connected before trying anything
		if (mBluetoothService.getState() != BluetoothService.STATE_CONNECTED) 
		{
			//Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT).show();//Keeps toasting even though data is coming in
			return;
		}

		// Check that there's actually something to send
		if (message.length() > 0) 
		{
			// Get the message bytes and tell the BluetoothChatService to write
			byte[] send = message.getBytes();
			mBluetoothService.write(send);

			// Reset out string buffer to zero and clear the edit text field
			//mOutStringBuffer.setLength(0);
			//mOutEditText.setText(mOutStringBuffer);
		}
	}

	// The Handler that gets information back from the BluetoothChatService
	private final Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case MESSAGE_STATE_CHANGE:
				if(D) Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
				switch (msg.arg1) {
				case BluetoothService.STATE_CONNECTED:
					//Manipulations related to connected state.
					//mConversationArrayAdapter.clear();
					break;
				case BluetoothService.STATE_CONNECTING:
					//Manipulations related to connecting state.
					break;
				case BluetoothService.STATE_LISTEN:
				case BluetoothService.STATE_NONE:
					//Manipulations related to not connected state.
					break;
				}
				break;
			case MESSAGE_WRITE:
				//byte[] writeBuf = (byte[]) msg.obj;
				// construct a string from the buffer
				//String writeMessage = new String(writeBuf);
				break;
			case MESSAGE_READ:
				byte[] readBuf = (byte[]) msg.obj;
				// construct a string from the valid bytes in the buffer
				String readMessage = new String(readBuf, 0, msg.arg1);
				isTransmitting = true;
				input.setText(readMessage);
				try
				{
					if(parseBTMessage(readMessage))//Parse message information
					{
						String comparator = readMessage.substring(0, 6);
						if(comparator.equals("BMA150"))//Compare to model number of sensor. Will need to change this for scalablity.
						{
							magnitude = determineMagnitude(values[0],values[1],values[2]);//Determine magnitude
						}
						//timeChange = determineTimeChange(timeStamp);//Determine change in time
						//distanceChange = determineDistanceChange(magnitude, timeChange);//Determine change in distance
						//fileOutMsg = readMessage + Double.toString(magnitude) + "\t" + Double.toString(timeChange) + "\t" + Double.toString(distanceChange);
						//pWriter.println(fileOutMsg);
					}
				}
				catch(NullPointerException e)
				{
					Log.e(TAG,"e");
					Log.e(TAG,"handleMessage:MESSAGE_READ does not work!");
				}
				//Update display
				break;
			case MESSAGE_DEVICE_NAME:
				// save the connected device's name
				mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
				Toast.makeText(getApplicationContext(), "Connected to "
						+ mConnectedDeviceName, Toast.LENGTH_SHORT).show();
				break;
			case MESSAGE_TOAST:
				Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),
						Toast.LENGTH_SHORT).show();
				break;
			}
		}
	};

	public void onActivityResult(int requestCode, int resultCode, Intent data) {
		if(D) Log.d(TAG, "onActivityResult " + resultCode);
		switch (requestCode) {
		case REQUEST_CONNECT_DEVICE:
			// When DeviceListActivity returns with a device to connect
			if (resultCode == Activity.RESULT_OK) {
				// Get the device MAC address
				String address = data.getExtras()
				.getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
				// Get the BLuetoothDevice object
				BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
				// Attempt to connect to the device
				mBluetoothService.connect(device);
			}
			break;
		case REQUEST_ENABLE_BT:
			// When the request to enable Bluetooth returns
			if (resultCode == Activity.RESULT_OK) {
				// Bluetooth is now enabled, so set up a chat session
				setupBTService();
			} else {
				// User did not enable Bluetooth or an error occured
				Log.d(TAG, "BT not enabled");
				Toast.makeText(this, R.string.bt_not_enabled_leaving, Toast.LENGTH_SHORT).show();
				finish();
			}
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.option_menu, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) 
	{
		switch (item.getItemId()) 
		{
		case R.id.scan:
			// Launch the DeviceListActivity to see devices and do scan
			Intent serverIntent = new Intent (this, DeviceListActivity.class);
			startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
			return true;

		case R.id.discoverable:
			// Ensure this device is discoverable by others
			ensureDiscoverable();
			return true;

		case R.id.calibrate:
			input.setText("Calibration in progress...");

			calibrationLoop = calibrationRange;
			accelLoop = 0;
			isCalibrating = true;

			while((calibrationLoop > 0))
			{
				if(parseBTMessage(bluetoothMessage))//Parse message information
				{
					String comparator = bluetoothMessage.substring(0, 6);
					if(comparator.equals("BMA150"))//Compare to model number of sensor. Will need to change this for scalablity.
					{
						calibMagnitude = determineMagnitude(values[0],values[1],values[2]) + calibMagnitude;//Determine magnitude
						accelLoop++;
					}
				}
				calibrationLoop--;
			}
			calibMagnitude = calibMagnitude/accelLoop;
			isCalibrating = false;
			return true;

		case R.id.sensortest://Simulate sensor captures but on local device and store
			isSensorTesting = !isSensorTesting;//Invert the boolean
		}
		return false;
	}

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) 
	{
		//Auto-generated method stub
	}

	@Override
	public void onSensorChanged(SensorEvent event) 
	{
		StringBuilder builder = new StringBuilder();
		bluetoothMessage = "";

		builder.append("--- SENSOR ---");
		builder.append("\nName: ");
		Sensor sensor = event.sensor;
		builder.append(sensor.getName());
		bluetoothMessage = bluetoothMessage + sensor.getName() + "\t";
		builder.append("\nType: ");
		builder.append(sensor.getType());
		bluetoothMessage = bluetoothMessage + sensor.getType() + "\t";
		builder.append("\nVendor: ");
		builder.append(sensor.getVendor());
		bluetoothMessage = bluetoothMessage + sensor.getVendor() + "\t";
		builder.append("\nVersion: ");
		builder.append(sensor.getVersion());
		bluetoothMessage = bluetoothMessage + sensor.getVersion() + "\t";
		builder.append("\nMaximum Range: ");
		builder.append(sensor.getMaximumRange());
		bluetoothMessage = bluetoothMessage + sensor.getMaximumRange() + "\t";
		builder.append("\nPower: ");
		builder.append(sensor.getPower());
		bluetoothMessage = bluetoothMessage + sensor.getPower() + "\t";
		builder.append("\nResolution: ");
		builder.append(sensor.getResolution());
		bluetoothMessage = bluetoothMessage + sensor.getResolution() + "\t";

		builder.append("\n\n--- EVENT ---");
		builder.append("\nAccuracy: ");
		builder.append(event.accuracy);
		bluetoothMessage = bluetoothMessage + event.accuracy + "\t";
		builder.append("\nTimestamp: ");
		builder.append(event.timestamp);
		bluetoothMessage = bluetoothMessage + event.timestamp + "\t";
		builder.append("\nValues:\n");


		for (int i = 0; i < event.values.length; i++) 
		{
			builder.append("   [");
			builder.append(i);
			builder.append("] = ");
			builder.append(event.values[i]);
			bluetoothMessage = bluetoothMessage + event.values[i] + "\t";
			builder.append("\n");
		}

		//Append latest calibrated values to message
		bluetoothMessage = bluetoothMessage + calibMagnitude;

		if(isCalibrating)
		{
			sensorReadingReady = true;
		}
		else if (isSensorTesting)
		{
			if (calibMagnitude != 0)
			{
				if (parseBTMessage(bluetoothMessage))//Parse message information
				{
					String comparator = bluetoothMessage.substring(0, 6);
					if (comparator.equals("BMA150"))//Compare to model number of sensor. Will need to change this for scalablity.
					{
						magnitude = determineMagnitude(values[0], values[1],
								values[2]);//Determine magnitude
					}
					timeChange = determineTimeChange(timeStamp);//Determine change in time
					distanceChange = determineDistanceChange(magnitude,
							timeChange);//Determine change in distance
					fileOutMsg = bluetoothMessage + Double.toString(magnitude)
					+ "\t" + Double.toString(timeChange) + "\t"
					+ Double.toString(distanceChange) + "\t" + Double.toString(calibMagnitude);
					pWriter.println(fileOutMsg);
				}
			}
			else if(!isTransmitting)
			{
				input.setText("PLEASE, CALIBRATE THE DEVICE FIRST!");
				isSensorTesting = false;
			}
		}
		else
		{
			sendMessage(bluetoothMessage);
			output.setText(builder);
			if(calibMagnitude != 0)
				input.setText("End of calibration. Scalar average = " + calibMagnitude);
		}
	}

	/**
	 * Takes message and extracts information from it and stores values in respective
	 * data types. Also, outputs the values to a local file on the SD card. For the
	 * mobile device it is used for the calibration process.
	 * NOTE: Meant to be used by base station application.
	 * @param message
	 * 		String that is passed from message handler that contains the contents of the bluetooth transmission.
	 */
	private boolean parseBTMessage(String message)
	{
		if(message != null)
		{
			Scanner scanner = new Scanner(message);
			scanner.useDelimiter("\t");
			if ( scanner.hasNext() )
			{
				try 
				{
					sensorName = scanner.next();
					sensorType = scanner.next();
					sensorVendor = scanner.next();
					sensorVersion = scanner.next();
					maxRange = scanner.next();
					powerDissipation = scanner.next();
					resolution = scanner.next();
					accuracy = scanner.next();
					timeStamp = scanner.next();
					values[0] = scanner.next();
					values[1] = scanner.next();
					values[2] = scanner.next();
				} 
				catch (NoSuchElementException e) 
				{
					e.printStackTrace();
					return false;
				}

				Log.d(TAG,"Name: "+ sensorName.trim() 
						+ ", Type: " + sensorType 
						+ ", Vendor: " + sensorVendor
						+ ", Version: " + sensorVersion
						+ ", Max Range: " + maxRange
						+ ", Power: " + powerDissipation
						+ ", Resolution: " + resolution
						+ ", accuracy: " + accuracy
						+ ", Timestamp: " + timeStamp 
						+ ", Values[0]: " + values[0]
						                           + ", Values[1]: " + values[1] 
						                                                      + ", Values[2]: " + values[2]);
			}
			else 
			{
				Log.d(TAG,"Sensor parser: Empty or invalid line. Unable to process.");
			}

			scanner.close();
		}

		return true;
	}

	/**
	 * Takes the sensor values of the accelerometer and determines the magnitude using
	 *  those values. For the mobile device, it is used for calibrating the sensors.
	 * NOTE: Meant to be used by base station application.
	 * @param x,y,z
	 */
	private double determineMagnitude(String x, String y, String z)
	{
		double newX = 0, newY = 0, newZ = 0;
		double sum;
		double result;

		//Convert strings to double
		newX = Double.valueOf(x);
		newY = Double.valueOf(y);
		newZ = (Double.valueOf(z));

		//Squaring each value
		newX = newX * newX;
		newY = newY * newY;
		newZ = newZ * newZ;

		//Add them together
		sum = (newX + newY + newZ);

		//Take the square root
		result = (Math.sqrt(sum));

		if(!isCalibrating)
		{
			result = result - calibMagnitude;
		}

		return(result);
	}

	/**
	 * Calculates the difference of the respective remote device's previous timestamp with the current timestamp. 
	 * NOTE: Meant to be used by base station application.
	 * @param newTime
	 */
	private double determineTimeChange(String newTime)
	{
		double timeChange = 0;
		double mNewTime = 0;

		//Convert to double
		mNewTime = Double.valueOf(newTime);
		mNewTime = (mNewTime/1000000000);

		newTimeStamp = mNewTime;

		timeChange = newTimeStamp - oldTimeStamp;

		//Store old time stamp.
		oldTimeStamp = newTimeStamp;

		Log.d(TAG,"Old Timestamp: "+ oldTimeStamp 
				+ " New Timestamp: " + newTimeStamp 
				+ " Time Change: " + timeChange);
		return timeChange;
	}

	/**
	 * With given time and magnitude the distance is calculated. 
	 * NOTE: Meant to be used by base station application.
	 * @param magnitude, timeChange
	 */
	//TODO: Redesign this function
	private double determineDistanceChange(Double mMagnitude, double mTimeChange)
	{
		distanceChange = 0;

		distanceChange = (double) (mMagnitude * (mTimeChange * mTimeChange));

		Log.d(TAG,"Magnitude: "+ mMagnitude 
				+ " Time Change: " + mTimeChange 
				+ " Distance Change: " + distanceChange);
		return distanceChange;
	}
}
